Speed up memo navigation: loading skeleton + parallel fetches - #62
Merged
Conversation
Clicking a memo felt unresponsive in production. Two causes: The memo route reads cookies (draft preview, viewer state), so it renders dynamically on every request. With no loading boundary its prefetch payload was empty (239 bytes in prod), so a click did a full round trip with the previous page still on screen and no feedback — indistinguishable from a dead click. Adding loading.tsx makes the skeleton statically prefetchable, so the click paints immediately. The render itself was also needlessly serial. fetchMemo and fetchMemos are independent but ran one after the other, and fetchMemos paged through all 65 memos a request at a time before fetching /team. Page 1 now establishes the page count and the rest go out together, with /team started up front. Measured against the live API, same output (65 memos, 81 team members): ~1.9s sequential to ~0.9s parallel. Also fixes a latent bug in the pagination loop, which mutated one shared queryParams object — safe only while the calls were sequential. Related memos are now non-fatal: that list backs a decorative two-item sidebar and shouldn't 500 the whole memo. Matches how /team failures were already handled here. A bad slug still 404s. The skeleton renders per-memo content as placeholders but keeps invariant chrome real — the Key Messages frame, eyebrow and numerals, the Signpost rail, track and share buttons — so the page reads as a memo mid-load rather than a generic loading card. Its primitives live in globals.css for reuse. This does not address the underlying dynamic rendering, the 65-memo fetch behind two related memos, or /team taking ~1.1s upstream. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Greptile SummaryThis PR adds prefetched loading skeletons to both memo-detail routes and reduces navigation latency by parallelizing independent memo, pagination, and team requests.
Confidence Score: 5/5The PR appears safe to merge, with no concrete changed-code defect identified. The parallel fetches preserve request parameters and result ordering, token setup completes before concurrent reads begin, and the new non-fatal related-memo behavior is intentional and documented.
|
| Filename | Overview |
|---|---|
| src/lib/api/memos.ts | Reworks pagination into ordered concurrent page requests, starts team metadata retrieval in parallel, and removes shared query-parameter mutation. |
| src/app/memos/[slug]/page.tsx | Fetches memo detail and the non-critical related-memo collection concurrently while retaining the existing not-found behavior. |
| src/app/toronto/memos/[slug]/page.tsx | Applies the same concurrent detail and related-list retrieval to Toronto-filtered memos. |
| src/app/memos/[slug]/MemoSkeleton.tsx | Introduces the shared accessible loading placeholder used by both memo-detail route variants. |
| src/app/globals.css | Adds reusable skeleton bar styles, sweep animation, emphasis, and stagger classes. |
| src/app/memos/[slug]/loading.tsx | Adds the default memo route loading boundary. |
| src/app/toronto/memos/[slug]/loading.tsx | Adds a Toronto-branded loading boundary with a back-link placeholder. |
Sequence Diagram
sequenceDiagram
participant Route as Memo route
participant Detail as fetchMemo
participant List as fetchMemos
participant API as York Factory API
Route->>Route: Prime preview token
par Requested memo
Route->>Detail: fetchMemo(slug)
Detail->>API: GET /memos/:slug
and Related memo list
Route->>List: fetchMemos(filters)
par Page count
List->>API: "GET /memos?page=1"
and Author metadata
List->>API: GET /team
end
API-->>List: Page 1 + total pages
par Remaining pages
List->>API: "GET /memos?page=2"
List->>API: "GET /memos?page=N"
end
end
Detail-->>Route: Memo or not found
List-->>Route: Ordered memos or empty related list
Reviews (1): Last reviewed commit: "Speed up memo navigation: loading skelet..." | Re-trigger Greptile
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Clicking a memo in production felt unresponsive. Investigating turned up two separate causes.
The click felt dead
The memo route reads cookies (
primeAdminPreviewToken+getCurrentUser), so it renders dynamically on every request. With no loading boundary, its prefetch payload was empty:So a click did a full origin round trip while React kept the previous page on screen — no spinner, no skeleton, nothing until the whole memo rendered. Adding
loading.tsxmakes the skeleton part of the statically prefetchable shell (239 bytes → ~30KB), so a hovered link paints instantly on click.This is compensation, not a cure — see "Not addressed" below.
The render was needlessly serial
fetchMemoandfetchMemosare independent but ran one after the other, andfetchMemospaged through all 65 memos one request at a time before fetching/team. Now page 1 establishes the page count, the rest go out together, and/teamstarts up front since it never depended on the pages.Measured against the live API, 3 runs each, identical output (65 memos, 81 team members):
This also fixes a latent bug: the pagination loop mutated a single shared
queryParamsobject and setpageon it. Safe only because the calls were sequential — parallelizing as-written would have had every request racing on the same key. Each page now builds its own params.The skeleton
Mirrors the real layout at the same widths and spacing so nothing shifts on swap. Chrome that's identical on every memo is rendered for real — the Key Messages frame, its eyebrow and
01/02/03numerals, the Signpost's accent rule, track, dots and share buttons — with placeholders only where per-memo content goes. Primitives (.skeleton-bar,-strong,.skeleton-delay-*) live inglobals.cssfor reuse; light grey from the charcoal ramp, with a sweep rather than an opacity pulse. Reduced-motion is already handled globally.Behaviour change worth a look
Related memos are now non-fatal (
fetchMemos().catch(() => [])). Previously a failure there would 500 the entire memo page over a decorative two-item sidebar. This matches how/teamfailures were already handled in the same file. A bad slug still 404s. Happy to make it fatal again if you'd rather.Testing
tsc --noEmitandeslintclean/memosstill lists all 65skeletonSweepconfirmed in compiled CSSnpm run buildfails on/tracker, which prerenders againstlocalhost:3000. Pre-existing and unrelated; the memo routes compile.Not addressed
Three larger items from the same investigation, in rough order of impact:
<Suspense>. This PR made it faster, not smaller./teamtakes ~1.1s upstream and is on nearly every page — now the long pole in the parallel version.🤖 Generated with Claude Code